/*
 * Element.java
 *
 * Created on 19 marzo 2004, 11.28
 */

package net.ermannofranco.xml.schema;

import net.ermannofranco.xml.Namespace;
import net.ermannofranco.xml.Static;
import net.ermannofranco.xml.SyntaxException;
import net.ermannofranco.xml.Tag;

/**
 * 
 * @author ermanno (ermanno.franco@gmail.com)
 */
public class Element extends SchemaTag {

	private static final long serialVersionUID = -4301426901249023656L;
	private boolean assigned;
	private boolean refAssigned;
	private boolean defFix;
	private boolean named;

	private static int UNBOUNDED_INT = Integer.MAX_VALUE;
	private static String UNBOUNDED_STR = "unbounded";
	private String nome;

	/**
	 * Creates a new instance of Element Protetto
	 * 
	 * @param level
	 *            livello di indentazione
	 * @param container
	 *            tag contenitore
	 */
	protected Element(int level, ISchemaTag container) {
		super("element", level, container);
	}

	/**
	 * Assegna il ref al nome di un altro elemento.
	 * 
	 * @param ref
	 * @return
	 */
	protected Element setRef(String ref) {
		if (named)
			throw new SchemaException("nome o ref gia' assegnato, "
					+ getClass().getName() + ":(" + getName() + ")");
		if (assigned)
			throw new SchemaException("tipo gia' assegnato, "
					+ getClass().getName() + ":(" + getName() + ")");
		Static.validateNMToken(ref);

		addAttr("ref", feedFormPrefix(ref));
		refAssigned = true;
		named = true;
		return this;
	}

	/**
	 * Assegna il ref ad un altro elemento, che deve avere un nome.
	 * 
	 * @param refEl
	 * @return
	 */
	public Element setRef(Element refEl) {
		return setRef(refEl.getName());
	}

	/**
	 * Shorcut for setName(name).setType(type);
	 * 
	 * @param name
	 * @param type
	 *            simple xml type enum
	 * @return
	 */
	public Element setNameAndType(String name, XmlType type) {
		setName(name).setType(type);
		return this;
	}

	/**
	 * Shorcut for setName(name).setType(type);
	 * 
	 * @param name
	 * @param type
	 *            reference to a declared simple or complex type
	 * @return
	 */
	public Element setNameAndType(String name, ContentType type) {
		setName(name).setType(type);
		return this;
	}

	/**
	 * Shorcut for setName(name).setType(type);
	 * 
	 * @param name
	 * @param typeName
	 *            type name as declared in document.
	 * @return
	 */
	public Element setNameAndType(String name, String typeName) {
		setName(name).setType(typeName);
		return this;
	}

	/**
	 * @param name
	 * @return
	 */
	public Element setName(String name) {
		if (named)
			throw new SchemaException("nome o ref gia' assegnato, "
					+ getClass().getName() + ":(" + getName() + ")");
		Static.validateNMToken(name);

		this.nome = name;
		addAttr("name", nome);
		named = true;
		return this;
	}

	/**
	 * @param simpleTypeNumber
	 * @return
	 */
	public Element setType(XmlType simpleTypeNumber) {
		if (refAssigned)
			throw new SchemaException("tipo non ammesso, ref gia' assegnato, "
					+ getClass().getName() + ":(" + getName() + ")");
		if (assigned)
			throw new SchemaException("tipo gia' assegnato, "
					+ getClass().getName() + ":(" + getName() + ")");
		String type = XmlType.getType(simpleTypeNumber);
		if (type == null)
			throw new SchemaException("tipo non esistente, "
					+ getClass().getName() + ":(" + getName() + ")");
		addAttr("type", getLocalNS() + ":" + type);
		assigned = true;
		return this;
	}

	/**
	 * 
	 * @param n
	 *            Namespace reference, the result is Namespaceprefix:typeName
	 * @param typeName
	 * @return
	 */
	public Element setType(Namespace n, String typeName) {
		if (refAssigned)
			throw new SchemaException("tipo non ammesso, ref gia' assegnato, "
					+ getClass().getName() + ":(" + getName() + ")");
		if (assigned)
			throw new SchemaException("tipo gia' assegnato, "
					+ getClass().getName() + ":(" + getName() + ")");
		Static.validateNMToken(typeName);

		addAttr("type", n.getPrefix() + Static.COLON + typeName);
		assigned = true;
		return this;
	}

	/**
	 * Error-safe method, assignment by reference, not by name...
	 * @param typeRef a declared type (simple or complex).
	 * @return this.
	 */
	public Element setType(ContentType typeRef) {
		setType(typeRef.getTypeName());
		return this;
	}

	/**
	 * @param typeName
	 * @return
	 */
	protected Element setType(String typeName) {
		if (refAssigned)
			throw new SchemaException("tipo non ammesso, ref gia' assegnato, "
					+ getClass().getName() + ":(" + getName() + ")");
		if (assigned)
			throw new SchemaException("tipo gia' assegnato, "
					+ getClass().getName() + ":(" + getName() + ")");
		Static.validateNMToken(typeName);

		addAttr("type", feedFormPrefix(typeName));
		assigned = true;
		return this;
	}

	/**
	 * @return
	 */
	public Attribute addAttributeTag() {
		Attribute att;
		add(att = new Attribute(getLevel() + 1, this));
		return att;
	}

	/**
	 * @param name
	 * @return
	 */
	public Unique addUnique(String name) {
		Unique att;
		add(att = new Unique(getLevel() + 1, this).setName(name));
		return att;
	}

	/**
	 * @param name
	 * @return
	 */
	public Key addKey(String name) {
		Key att;
		add(att = new Key(getLevel() + 1, this).setName(name));
		return att;
	}

	/**
	 * @param name
	 * @param refer
	 * @return
	 */
	public Keyref addKeyref(String name, String refer) {
		Keyref att;
		add(att = new Keyref(getLevel() + 1, this).setName(name)
				.setRefer(refer));
		return att;
	}

	/**
	 * add maxOccurs="unbounded"
	 * @return
	 */
	public Element setUnboundedMaxOccurs() {
		setMaxOccurs(UNBOUNDED_INT);
		return this;
	}

	/**
	 * Allow zero or unbounded occurs. Shortcut for
	 * setMinOccurs(0).setUnboundedMaxOccurs().
	 * 
	 * @return this.
	 */
	public Element setAllPossibleOccurs() {
		return setMinOccurs(0).setUnboundedMaxOccurs();
	}

	/**
	 * Shortcut for setMinOccurs(0).setMaxOccurs(1). Remember: if maxOccurs is
	 * not specified in element, default is 1 anyway.
	 * 
	 * @return this.
	 */
	public Element setZeroOrOneOccurs() {
		return setMinOccurs(0).setMaxOccurs(1);
	}

	/**
	 * Shortcut for setMinOccurs(0).
	 * 
	 * @return this.
	 */
	public Element setZeroMinOccurs() {
		return setMinOccurs(0);
	}

	/**
	 * @param minOccurs
	 * @return
	 */
	public Element setMinOccurs(int minOccurs) {
		if (minOccurs < 0)
			throw new SyntaxException(minOccurs + " non valido in minOccurs: "
					+ getName());
		if (getLevel() == 1)
			throw new SchemaException("minOccurs non valido al primo livello: "
					+ getName());// al 1 livello non e' ammesso
		addAttr("minOccurs", "" + minOccurs);// al 1 livello non e' ammesso
		return this;
	}

	/**
	 * @param maxOccurs
	 * @return
	 */
	public Element setMaxOccurs(int maxOccurs) {
		if (maxOccurs < 1)
			throw new SyntaxException(maxOccurs + " non valido in maxOccurs: "
					+ getName());
		if (getLevel() == 1)
			throw new SchemaException("maxOccurs non valido al primo livello: "
					+ getName());// al 1 livello non e' ammesso
		if (maxOccurs == UNBOUNDED_INT) {
			addAttr("maxOccurs", UNBOUNDED_STR);
		} else {
			addAttr("maxOccurs", String.valueOf(maxOccurs));
		}
		return this;
	}

	/**
	 * @param flag
	 * @return
	 */
	public Element setNillable(boolean flag) {
		addAttr("nillable", "" + flag);
		return this;
	}

	/**
	 * @param dflt
	 * @return
	 */
	public Element setDefault(String dflt) {
		if (defFix)
			throw new SchemaException(
					"Default e fixed sono mutuamente esclusivi, " + getName());
		addAttr("default", dflt);
		defFix = true;
		return this;
	}

	/**
	 * @param fixed
	 * @return
	 */
	public Element setFixed(String fixed) {
		if (defFix)
			throw new SchemaException(
					"Default e fixed sono mutuamente esclusivi, " + getName());
		// clear();//attenz agli attr tolti...
		addAttr("fixed", fixed);
		defFix = true;
		return this;
	}

	// override per restituire il nome dell'elemento
	// altrimenti restituirebbe 'element'
	/**
	 * @return
	 */
	public String getName() {
		return nome;
	}

	/*
	 * private void clear() { removeAttribute("maxOccurs");
	 * removeAttribute("minOccurs"); removeAttribute("default");
	 * 
	 * }
	 */
	/*
	 * public final Schema getSchema() { //Schema estende Tag, usa questo metodo
	 * perche' final //in Schema il container e' null, e' l'unico.
	 * 
	 * if(getContainer().getClass().equals(Schema.class)) { return
	 * (Schema)getContainer(); } //ricorsivo, risale l'albero finche' trova
	 * return ((Element)getContainer()).getSchema(); }
	 */
	/**
	 * @return
	 */
	public SimpleType setSimpleType() {
		if (refAssigned)
			throw new SchemaException("tipo non ammesso, ref gia' assegnato, "
					+ getClass().getName() + ":(" + getName() + ")");
		if (assigned)
			throw new SchemaException("contenuto gia' assegnato, "
					+ getClass().getName() + ":(" + getName() + ")");
		SimpleType att = new SimpleType(getLevel() + 1, this);
		add(att);// vado ad aggiungerlo all'ArrayList della superclasse Tag
		assigned = true;
		return att;
	}

	/**
	 * @return
	 */
	public ComplexType setComplexType() {
		if (refAssigned)
			throw new SchemaException("tipo non ammesso, ref gia' assegnato, "
					+ getClass().getName() + ":(" + getName() + ")");
		if (assigned)
			throw new SchemaException("contenuto gia' assegnato, "
					+ getClass().getName() + ":(" + getName() + ")");
		ComplexType ct = new ComplexType(getLevel() + 1, this);
		add(ct);// vado ad aggiungerlo all'ArrayList della superclasse Tag
		assigned = true;
		return ct;
	}

	/**
	 * aggiunge un tag Annotation che contiene un tag Documentation
	 * 
	 * @param explanation
	 *            testo della documentazione
	 * @return riferimento all'oggetto corrente
	 */
	public Documentation addDocumentation(String explanation) {
		return addAnnotation().addDocumentation(explanation);
	}
	public Documentation addDocumentation() {
		return addAnnotation().addDocumentation();
	}
	/**
	 * aggiunge un tag Annotation che contiene un tag Documentation
	 * 
	 * @param explanation
	 *            testo della documentazione
	 * @param language
	 *            attributo di xml:lang, seguire le regole di i18n
	 * @return riferimento all'oggetto corrente
	 */
	public Documentation addDocumentation(String explanation, String language) {
		return addAnnotation().addDocumentation(explanation, language);
	}

	private void validateElement() {
		if (refAssigned && getLevel() == 1)
			throw new SchemaException("ref non ammesso al primo livello, "
					+ getClass().getName() + ":(" + getName() + ")");
		if (!named && getLevel() == 1)
			throw new SchemaException("nome necessario al primo livello, "
					+ getClass().getName() + ":(" + getName() + ")");
		if (!refAssigned && !assigned)
			throw new SchemaException(
					"specificare il tipo di elemento o assegnare un riferimento, "
							+ getClass().getName() + ":(" + getName() + ")");
		if (!(named || refAssigned))
			throw new SchemaException("specificare uno tra name e ref, "
					+ getClass().getName() + ":(" + getName() + ")");
	}

	/**
	 * semplice override di convalida
	 * 
	 * @return
	 */
	public String toPrint() {
		validateElement();
		return super.toPrint();
	}
	public String toSave() {
		validateElement();
		return super.toSave();
	}
}
